Skip to content

docs(guide): author the page-mode navigate actions where the renderer reads them (objectui#7440) - #7865

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7440-record-edit-modes-nested-action-bag
Sep 6, 2026
Merged

docs(guide): author the page-mode navigate actions where the renderer reads them (objectui#7440)#7865
os-sam merged 1 commit into
mainfrom
claude/issue-7440-record-edit-modes-nested-action-bag

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7440

content/docs/guide/record-edit-modes.md taught three action:button examples that put the action name inside a nested action bag. Nothing reads that key. This rewrites all three into the shape a renderer actually reads, derived from the code and verified by rendering the published snippets.

The handlers are live and untouched. navigate_create / navigate_edit really are registered at packages/app-shell/src/console/AppContent.tsx:475,487. Nothing in this PR deletes, retires or re-registers them. Only the doc page changed — one file, 24 insertions, 15 deletions.

Where the working shape came from (code, not the card)

step site what it says
the renderer's forward whitelist packages/components/src/renderers/action/action-button.tsx:160 type: schema.actionType — and schema.action is read nowhere in the file
its params leg action-button.tsx:126-128,173 params: schema.params (object) or actionParams (array)
the runner's dispatch packages/core/src/actions/ActionRunner.ts:972 action.type || action.actionType || action.name
the handler lookup ActionRunner.ts:1064 this.handlers.has(actionType)
what the handler reads packages/app-shell/src/utils/recordFormNavigation.ts:271,300 action.params?.objectName, action.params?.recordId

⇒ the authored channel is actionType for the handler name and a top-level params object for its arguments. The nested bag forwards type: undefined and matches no handler: the button renders, is clickable, does nothing.

Positive control, in-repo and live: packages/components/src/renderers/action/__tests__/action-forward-precedence.test.tsx:67-82 pins exactly that composition (actionType + top-level params), and packages/components/src/__tests__/action-bodyExtra-forward.test.tsx:88-95 authors an action:button the same way. Both are green today.

Before / after, per example

⚠️ Not one blanket rewrite. Two of the three were simply mis-authored; the third was demonstrating a capability, and one of the two needed more than a key move.

1. navigate_create with an explicit object — mis-authored, mechanical rewrite

 {
   "type": "action:button",
   "label": "New Account",
   "icon":  "plus",
-  "action": {
-    "action": "navigate_create",
-    "params": { "objectName": "account" }
-  }
+  "actionType": "navigate_create",
+  "params": { "objectName": "account" }
 }

2. navigate_edit — same rewrite, plus its record id had a second defect

 {
   "type": "action:button",
   "label": "Edit",
   "icon":  "pencil",
-  "action": {
-    "action": "navigate_edit",
-    "params": {
-      "objectName": "account",
-      "recordId":   "${record.id}"
-    }
+  "actionType": "navigate_edit",
+  "params": {
+    "objectName": "account",
+    "recordId":   "0015e000abcd"
   }
 }

A purely mechanical rewrite here would have published a second silently-inert example. Measured through the real renderers, with a live record bound:

control  properties.label = "L-${record.id}"     -> DOM shows "L-rec_1"          RESOLVES (instrument fires)
case A   node-level params.recordId              -> handler got "${record.id}"   RAW
case B   params inside the `properties` bag      -> handler got "${record.id}"   RAW

The properties hoist evaluates per value and shallowly, so a template nested one level inside a params object is never reached — and action:button deliberately does not inject the surrounding row (packages/app-shell/src/views/DeclaredActionsBar.tsx:110: "injects the record under params._rowRecord — which action:button does NOT do"), while resolveNavigateEditUrl has no context fallback for recordId by design (recordFormNavigation.ts:286-287). So from metadata the id can only be a literal. The example now says so, and points per-row Edit at the built-in entry point the page already documents.

3. context-supplied object — demonstrating a capability, so the repair keeps the omission

 {
   "type": "action:button",
   "label": "New",
-  "action": { "action": "navigate_create" }
+  "actionType": "navigate_create"
 }

The point of this one is that params may be omitted and the action context supplies objectName (recordFormNavigation.ts:271, last precedence step). Filling in a params bag to "make it work" would have deleted what it teaches. It stays absent.

⭐ Why nothing went red — measured, not assumed

The three blocks are ```jsonc fences. Exactly one instrument reads them, and it reads one key.

scripts/check-doc-component-types.mjs walks every fence in content/docs/** regardless of language and collects type sites with a regex accepting "type", 'type', or a bare type guarded by a negative lookbehind on word characters, $ and . (line 1085 of that file) — so it saw "type": "action:button", judged it registered, and passed. Its own header states the limit verbatim: "NOT in scope, deliberately: whether the snippet's OTHER keys are read by the renderer the type resolves to."

Two mutations on the pristine tree, each proved on disk before the run and restored to the HEAD blob after:

M1  "action": "navigate_create"  ->  "action": "zzz_bogus_not_a_handler"   (2 injected, 0 originals left)
    check:doc-types EXIT=0 — identical counters (1106 code blocks, 897 type literals, 778 registered)   ⇒ BLIND

M2  "type": "action:button"      ->  "type": "action:zzz-not-registered"   (3 injected, 0 originals left)
    check:doc-types EXIT=1 —
      content/docs/guide/record-edit-modes.md:70   [unregistered-doc-type]  (jsonc)
      content/docs/guide/record-edit-modes.md:82   [unregistered-doc-type]  (jsonc)
      content/docs/guide/record-edit-modes.md:101  [unregistered-doc-type]  (jsonc)                      ⇒ FIRES

M2 names this file, these three blocks, and reports their fence as jsonc — so the instrument is alive on exactly the blocks in question and blind to exactly the key that was wrong. The gate's counters are byte-identical before and after this PR's repair, which is the same fact from the other side.

No gate is added here. objectui#7851 is in flight and is exactly that report-only sweep of non-type keys in content/docs/** json fences; this page is a known corpus point for it — the three blocks are jsonc, not json, which that sweep's fence-language set should cover.

Evidence

Union re-run after the final commit, at de0205717, worktree clean. Exit codes captured by redirecting first, never through a pipe; verdicts quoted from each gate's own line.

command exit verdict line
pnpm check:doc-types 0 ✅ Every documented component type is registered.
pnpm check:doc-fences 0 ✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript …
pnpm check:doc-snippets 0 Every covered documentation snippet compiles against the built types. (456 of 456 blocks judged, 0 failed)
pnpm check:control-bytes 0 ✅ check-control-bytes: OK (scanned 6367 tracked text file(s); skipped 85 binary).
pnpm check:shell-escape-residue 0 ✅ check-shell-escape-residue: OK (5/5 root(s) resolved … content/docs: 184 file(s), 1065 fence(s) …)
pnpm docs:check-links 0 Links are valid across 17 scan roots.
pnpm check:docs-route-closure 0 ✅ all 13 packages named in … registerCatalogBlocks.ts are accounted for
node scripts/check-changeset-presence.mjs 0 ✅ No source or published contract of a released package changed in this range, so no changeset is owed.

check:doc-snippets first returned EXIT=2 — PRECONDITION NOT MET (unbuilt packages). That is "I could not run", not a red gate, so it is not reported as one: the scoped closure was built first (pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2 — 34/34 tasks, 4m22s, exit 0) and the gate re-run for the real reading above.

Runtime verification of the published snippets. A throwaway probe read the three ```jsonc blocks back out of the committed file, JSON.parsed them, and rendered each through `SchemaRenderer` with `navigate_create` / `navigate_edit` registered the way `AppContent.tsx` registers them:

BLOCKS_FOUND=3  PARSED_OK=3
EX1  {"type":"action:button","label":"New Account","icon":"plus","actionType":"navigate_create","params":{"objectName":"account"}}
     -> navigate_create called once, params {"objectName":"account"}
EX2  {"type":"action:button","label":"Edit","icon":"pencil","actionType":"navigate_edit","params":{"objectName":"account","recordId":"0015e000abcd"}}
     -> navigate_edit called once, params {"objectName":"account","recordId":"0015e000abcd"}, no "${" residue
EX3  {"type":"action:button","label":"New","actionType":"navigate_create"}
     -> navigate_create called once, params undefined

and the negative control, the shape the page taught before this PR:

{"type":"action:button","label":"New Account","icon":"plus","action":{"action":"navigate_create","params":{…}}}
     -> navigate_create NOT called

The probe files were deleted before the commit; git status --porcelain is empty and the diff is one file. Downstream of the forward, packages/app-shell/src/utils/__tests__/recordFormNavigation.test.ts (37 tests) passes unchanged — that is the suite that turns action.params into the URLs this page documents.

Not run, and why: the full pnpm test and pnpm lint farms (CI owns them; this diff is one markdown file and adds no code), and the remaining 30-odd check:* gates whose inputs (package source, manifests, i18n bundles, skills, spec symbols) this diff does not touch.

Scope

Clause-② stays no: no new authorable key, no handler-registration change, no gate change, no renderer change. One file.

Two findings outside this card's surface were filed rather than repaired here — see the comment on #7440.


Generated by Claude Code

… reads them (objectui#7440)

`record-edit-modes.md` taught three `action:button` examples that put the
action name in a nested `action` bag. `action-button.tsx` never reads
`schema.action`: it forwards `type: schema.actionType` and `params:
schema.params`, and `ActionRunner.execute` dispatches on
`action.type || action.actionType || action.name`. The documented shape
therefore forwarded `type: undefined` and matched no handler — a button
that rendered, clicked, and did nothing.

The `navigate_create` / `navigate_edit` handlers are live
(`AppContent.tsx:475,487`); only the authoring shape was wrong. Each
example is repaired for what it was demonstrating:

- example 1 (`navigate_create` with an explicit object): mis-authored,
  rewritten to `actionType` + top-level `params`.
- example 2 (`navigate_edit`): same rewrite, and its `${record.id}`
  replaced with a literal id. Measured: `params` is not template-
  evaluated on any authoring channel, and `action:button` does not
  inject the surrounding row (`DeclaredActionsBar.tsx:110`), so the
  template arrived at the handler verbatim.
- example 3 (context-supplied object): demonstrates omitting the
  arguments, so the repair keeps `params` absent rather than filling it
  in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants